package com.gradecak.alfresco.mvc.servlet;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.support.HandlerMethodResolver;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
import org.springframework.web.util.UrlPathHelper;


/**
 * Servlet-specific subclass of {@link HandlerMethodResolver}.
 */
class ServletHandlerMethodResolver extends HandlerMethodResolver {

	private UrlPathHelper urlPathHelper = new UrlPathHelper();

	private PathMatcher pathMatcher = new AntPathMatcher();
	
	private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver();

	final private Class<?> handlerType;

	ServletHandlerMethodResolver(Class<?> handlerType) {
		this.handlerType = handlerType;
		init(handlerType);
	}

	public Method resolveHandlerMethod(FrameworkServletRequestProxy request)
			throws ServletException {

		String lookupPath = urlPathHelper.getLookupPathForRequest(request);
		Comparator<String> pathComparator = pathMatcher
				.getPatternComparator(lookupPath);
		Map<RequestMappingInfo, Method> targetHandlerMethods = new LinkedHashMap<RequestMappingInfo, Method>();
		Set<String> allowedMethods = new LinkedHashSet<String>(7);
		String resolvedMethodName = null;
		RequestMapping clazzMapping = AnnotationUtils.findAnnotation(
				handlerType, RequestMapping.class);

		for (Method handlerMethod : getHandlerMethods()) {

			RequestMappingInfo mappingInfo = new RequestMappingInfo();
			RequestMapping mapping = AnnotationUtils.findAnnotation(
					handlerMethod, RequestMapping.class);
			mappingInfo.paths = mapping.value();
			if (!hasTypeLevelMapping()
					|| !Arrays.equals(mapping.method(), getTypeLevelMapping()
							.method())) {
				mappingInfo.methods = mapping.method();
			}
			if (!hasTypeLevelMapping()
					|| !Arrays.equals(mapping.params(), getTypeLevelMapping()
							.params())) {
				mappingInfo.params = mapping.params();
			}
			if (!hasTypeLevelMapping()
					|| !Arrays.equals(mapping.headers(), getTypeLevelMapping()
							.headers())) {
				mappingInfo.headers = mapping.headers();
			}

			System.out.println(handlerMethod);

			System.out.println(mapping.value().toString());
			System.out.println(request.getRequestURI());

			boolean match = false;
			if (mappingInfo.paths.length > 0) {
				List<String> matchedPaths = new ArrayList<String>(
						mappingInfo.paths.length);
				for (String methodLevelPattern : mappingInfo.paths) {
					String matchedPattern = getMatchedPattern(
							methodLevelPattern, lookupPath, request);
					if (matchedPattern != null) {
						if (mappingInfo.matches(request)) {
							match = true;
							matchedPaths.add(matchedPattern);
						} else {
							for (RequestMethod requestMethod : mappingInfo.methods) {
								allowedMethods.add(requestMethod.toString());
							}
							break;
						}
					}
				}
				Collections.sort(matchedPaths, pathComparator);
				mappingInfo.matchedPaths = matchedPaths;
			} else {
				// No paths specified: parameter match sufficient.
				match = mappingInfo.matches(request);
				if (match && mappingInfo.methods.length == 0
						&& mappingInfo.params.length == 0
						&& resolvedMethodName != null
						&& !resolvedMethodName.equals(handlerMethod.getName())) {
					match = false;
				} else {
					for (RequestMethod requestMethod : mappingInfo.methods) {
						allowedMethods.add(requestMethod.toString());
					}
				}
			}
			if (match) {
				Method oldMappedMethod = targetHandlerMethods.put(mappingInfo,
						handlerMethod);
				if (oldMappedMethod != null && oldMappedMethod != handlerMethod) {
					if (methodNameResolver != null
							&& mappingInfo.paths.length == 0) {
						if (!oldMappedMethod.getName().equals(
								handlerMethod.getName())) {
							if (resolvedMethodName == null) {
								resolvedMethodName = methodNameResolver
										.getHandlerMethodName(request);
							}
							if (!resolvedMethodName.equals(oldMappedMethod
									.getName())) {
								oldMappedMethod = null;
							}
							if (!resolvedMethodName.equals(handlerMethod
									.getName())) {
								if (oldMappedMethod != null) {
									targetHandlerMethods.put(mappingInfo,
											oldMappedMethod);
									oldMappedMethod = null;
								} else {
									targetHandlerMethods.remove(mappingInfo);
								}
							}
						}
					}
					if (oldMappedMethod != null) {
						throw new IllegalStateException(
								"Ambiguous handler methods mapped for HTTP path '"
										+ lookupPath
										+ "': {"
										+ oldMappedMethod
										+ ", "
										+ handlerMethod
										+ "}. If you intend to handle the same path in multiple methods, then factor "
										+ "them out into a dedicated handler class with that path mapped at the type level!");
					}
				}
			}
		}
		if (!targetHandlerMethods.isEmpty()) {
			List<RequestMappingInfo> matches = new ArrayList<RequestMappingInfo>(
					targetHandlerMethods.keySet());
			RequestMappingInfoComparator requestMappingInfoComparator = new RequestMappingInfoComparator(
					pathComparator);
			Collections.sort(matches, requestMappingInfoComparator);
			RequestMappingInfo bestMappingMatch = matches.get(0);
			String bestMatchedPath = bestMappingMatch.bestMatchedPath();
			if (bestMatchedPath != null) {
				extractHandlerMethodUriTemplates(bestMatchedPath, lookupPath,
						request);
			}
			return targetHandlerMethods.get(bestMappingMatch);
		} else {
			if (!allowedMethods.isEmpty()) {
				throw new HttpRequestMethodNotSupportedException(
						request.getMethod(),
						StringUtils.toStringArray(allowedMethods));
			} else {
				throw new NoSuchRequestHandlingMethodException(lookupPath,
						request.getMethod(), request.getParameterMap());
			}
		}
	}
	
	private String getMatchedPattern(String methodLevelPattern, String lookupPath, HttpServletRequest request) {
		if (hasTypeLevelMapping() && (!ObjectUtils.isEmpty(getTypeLevelMapping().value()))) {
			String[] typeLevelPatterns = getTypeLevelMapping().value();
			for (String typeLevelPattern : typeLevelPatterns) {
				if (!typeLevelPattern.startsWith("/")) {
					typeLevelPattern = "/" + typeLevelPattern;
				}
				String combinedPattern = pathMatcher.combine(typeLevelPattern, methodLevelPattern);
				if (isPathMatchInternal(combinedPattern, lookupPath)) {
					return combinedPattern;
				}
			}
			return null;
		}
		String bestMatchingPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
		if (StringUtils.hasText(bestMatchingPattern)) {
			String combinedPattern = pathMatcher.combine(bestMatchingPattern, methodLevelPattern);
			if (!combinedPattern.equals(bestMatchingPattern) &&
					(isPathMatchInternal(combinedPattern, lookupPath))) {
				return combinedPattern;
			}
		}
		if (isPathMatchInternal(methodLevelPattern, lookupPath)) {
			return methodLevelPattern;
		}
		return null;
	}
	
	private boolean isPathMatchInternal(String pattern, String lookupPath) {
		if (pattern.equals(lookupPath) || pathMatcher.match(pattern, lookupPath)) {
			return true;
		}
		boolean hasSuffix = pattern.indexOf('.') != -1;
		if (!hasSuffix && pathMatcher.match(pattern + ".*", lookupPath)) {
			return true;
		}
		boolean endsWithSlash = pattern.endsWith("/");
		if (!endsWithSlash && pathMatcher.match(pattern + "/", lookupPath)) {
			return true;
		}
		return false;
	}
	
	@SuppressWarnings("unchecked")
	private void extractHandlerMethodUriTemplates(String mappedPath,
			String lookupPath,
			HttpServletRequest request) {
		Map<String, String> variables = null;
		boolean hasSuffix = (mappedPath.indexOf('.') != -1);
		if (!hasSuffix && pathMatcher.match(mappedPath + ".*", lookupPath)) {
			String realPath = mappedPath + ".*";
			if (pathMatcher.match(realPath, lookupPath)) {
				variables = pathMatcher.extractUriTemplateVariables(realPath, lookupPath);
			}
		}
		if (variables == null && !mappedPath.startsWith("/")) {
			String realPath = "/**/" + mappedPath;
			if (pathMatcher.match(realPath, lookupPath)) {
				variables = pathMatcher.extractUriTemplateVariables(realPath, lookupPath);
			}
			else {
				realPath = realPath + ".*";
				if (pathMatcher.match(realPath, lookupPath)) {
					variables = pathMatcher.extractUriTemplateVariables(realPath, lookupPath);
				}
			}
		}
		if (!CollectionUtils.isEmpty(variables)) {
			Map<String, String> typeVariables =
					(Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
			if (typeVariables != null) {
				variables.putAll(typeVariables);
			}
			request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, variables);
		}
	}

}
